#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@Project : audit
@File : Tomcat_rules.py
@Author :
@Time : 2025/11/3 15:30
@脚本说明 : Tomcat安全加固规则类
"""

import subprocess
import re
from typing import Dict, List, Any
import logging

logger = logging.getLogger(__name__)


class TomcatSecurityRules:
    """Tomcat环境下安全加固规则类"""

    TOMCAT_INSTALL_PATH = "/opt/tomcat8/"  # 固定Tomcat安装路径

    @staticmethod
    def run_cmd_simple(command: str) -> str:
        """简化的命令执行，处理Linux编码问题"""
        try:
            result = subprocess.run(
                command,
                shell=True,
                capture_output=True,
                timeout=30,
                text=True
            )
            return result.stdout.strip() or result.stderr.strip()
        except Exception as e:
            return f"Error: {str(e)}"

    @staticmethod
    def find_tomcat_config() -> str:
        """查找Tomcat配置文件路径"""
        config_path = f"{TomcatSecurityRules.TOMCAT_INSTALL_PATH}/conf/server.xml"
        if TomcatSecurityRules.run_cmd_simple(f"test -f {config_path} && echo 1 || echo 0") == "1":
            return config_path
        return ""

    @staticmethod
    def check_strong_password() -> Dict[str, Any]:
        """1. 检查Tomcat账户强密码配置"""
        print("[Tomcat规则1] 检查账户强密码配置...")
        status_details = []
        issues = []
        users_config = f"{TomcatSecurityRules.TOMCAT_INSTALL_PATH}/conf/tomcat-users.xml"

        # 检查用户配置文件是否存在
        if TomcatSecurityRules.run_cmd_simple(f"test -f {users_config} && echo 1 || echo 0") != "1":
            issues.append("未找到tomcat-users.xml配置文件")
            status_details.append("✗ 配置文件: 未找到用户配置文件")
            return {
                "rule_name": "账户强密码检查",
                "status": "未通过",
                "config_status": {"config_exists": False},
                "status_details": status_details,
                "recommendation": "请确认Tomcat用户配置文件位置"
            }

        # 检查是否存在管理员用户
        content = TomcatSecurityRules.run_cmd_simple(f"cat {users_config}")
        user_entries = re.findall(r'<user\s+.*?/>', content, re.IGNORECASE)
        has_admin_user = any("manager" in entry or "admin" in entry for entry in user_entries)

        if not has_admin_user:
            issues.append("未配置管理员用户")
            status_details.append("✗ 账户配置: 未发现管理员用户")
        else:
            # 检查密码强度（简单判断）
            weak_passwords = []
            for entry in user_entries:
                password_match = re.search(r'password="([^"]+)"', entry)
                username_match = re.search(r'username="([^"]+)"', entry)
                if password_match and username_match:
                    password = password_match.group(1)
                    username = username_match.group(1)
                    if len(password) < 8 or not re.search(r'[A-Z]', password) or \
                       not re.search(r'[a-z]', password) or not re.search(r'[0-9]', password):
                        weak_passwords.append(username)

            if weak_passwords:
                issues.append(f"以下用户密码强度不足: {', '.join(weak_passwords)}")
                status_details.append(f"✗ 密码强度: {', '.join(weak_passwords)}密码不符合强密码要求")
            else:
                status_details.append("✓ 密码强度: 所有用户密码符合强密码要求")

        return {
            "rule_name": "账户强密码检查",
            "status": "通过" if not issues else "未通过",
            "config_status": {
                "config_exists": True,
                "has_admin_user": has_admin_user,
                "weak_password_count": len(weak_passwords) if 'weak_passwords' in locals() else 0
            },
            "status_details": status_details,
            "recommendation": "账户密码配置符合安全要求" if not issues else
            "需为所有用户配置强密码（长度≥8位，包含大小写字母、数字）并确保存在管理员账户"
        }

    @staticmethod
    def check_minimal_permissions() -> Dict[str, Any]:
        """2. 检查最小化权限配置"""
        print("[Tomcat规则2] 检查最小化权限配置...")
        status_details = []
        issues = []

        # 检查Tomcat目录是否存在
        if TomcatSecurityRules.run_cmd_simple(f"test -d {TomcatSecurityRules.TOMCAT_INSTALL_PATH} && echo 1 || echo 0") != "1":
            issues.append(f"Tomcat安装目录{TomcatSecurityRules.TOMCAT_INSTALL_PATH}不存在")
            status_details.append(f"✗ 目录检查: {TomcatSecurityRules.TOMCAT_INSTALL_PATH} 不存在")
            return {
                "rule_name": "最小化权限检查",
                "status": "未通过",
                "config_status": {"dir_exists": False},
                "status_details": status_details,
                "recommendation": "请确认Tomcat安装目录"
            }

        # 检查运行用户（通常在systemd服务文件中配置）
        run_user = "tomcat"  # 默认运行用户
        service_check = TomcatSecurityRules.run_cmd_simple(
            "grep 'User=' /etc/systemd/system/tomcat.service 2>/dev/null || "
            "grep 'TOMCAT_USER' /etc/init.d/tomcat 2>/dev/null"
        )
        user_match = re.search(r'(User=|TOMCAT_USER=)(\w+)', service_check)
        if user_match:
            run_user = user_match.group(2)

        # 检查关键目录权限
        key_dirs = [
            f"{TomcatSecurityRules.TOMCAT_INSTALL_PATH}/bin",
            f"{TomcatSecurityRules.TOMCAT_INSTALL_PATH}/conf",
            f"{TomcatSecurityRules.TOMCAT_INSTALL_PATH}/webapps",
            f"{TomcatSecurityRules.TOMCAT_INSTALL_PATH}/logs",
            f"{TomcatSecurityRules.TOMCAT_INSTALL_PATH}/temp",
            f"{TomcatSecurityRules.TOMCAT_INSTALL_PATH}/work"
        ]

        for dir_path in key_dirs:
            if TomcatSecurityRules.run_cmd_simple(f"test -d {dir_path} && echo 1 || echo 0") == "1":
                perm_check = TomcatSecurityRules.run_cmd_simple(f"stat -c '%a %U' {dir_path}")
                permissions, owner = perm_check.split() if ' ' in perm_check else ("", "")

                if permissions and int(permissions) > 750:
                    issues.append(f"目录{dir_path}权限过高({permissions})")
                    status_details.append(f"✗ 目录权限: {dir_path} 权限为{permissions}（建议≤750）")
                else:
                    status_details.append(f"✓ 目录权限: {dir_path} 权限为{permissions}")

                if owner != run_user:
                    issues.append(f"目录{dir_path}所有者为{owner}，与运行用户{run_user}不一致")
                    status_details.append(f"✗ 目录所有者: {dir_path} 所有者为{owner}（建议改为{run_user}）")
                else:
                    status_details.append(f"✓ 目录所有者: {dir_path} 所有者为{owner}")
            else:
                issues.append(f"关键目录{dir_path}不存在")
                status_details.append(f"✗ 目录检查: {dir_path} 不存在")

        # 检查配置文件权限
        config_files = [
            f"{TomcatSecurityRules.TOMCAT_INSTALL_PATH}/conf/server.xml",
            f"{TomcatSecurityRules.TOMCAT_INSTALL_PATH}/conf/tomcat-users.xml"
        ]
        for file_path in config_files:
            if TomcatSecurityRules.run_cmd_simple(f"test -f {file_path} && echo 1 || echo 0") == "1":
                perm_check = TomcatSecurityRules.run_cmd_simple(f"stat -c '%a' {file_path}")
                if perm_check and int(perm_check) > 640:
                    issues.append(f"配置文件{file_path}权限过高({perm_check})")
                    status_details.append(f"✗ 配置文件权限: {file_path} 权限为{perm_check}（建议≤640）")
                else:
                    status_details.append(f"✓ 配置文件权限: {file_path} 权限为{perm_check}")

        return {
            "rule_name": "最小化权限检查",
            "status": "通过" if not issues else "未通过",
            "config_status": {
                "run_user": run_user,
                "key_dirs_count": len(key_dirs),
                "secure_perm_count": len(status_details) - len([d for d in status_details if "✗" in d])
            },
            "status_details": status_details,
            "recommendation": "权限配置符合最小化原则" if not issues else f"需修复: {'; '.join(issues)}"
        }

    @staticmethod
    def check_log_audit() -> Dict[str, Any]:
        """3. 检查日志审计配置"""
        print("[Tomcat规则3] 检查日志审计配置...")
        status_details = []
        issues = []
        config_path = TomcatSecurityRules.find_tomcat_config()

        if not config_path:
            issues.append("未找到Tomcat配置文件")
            status_details.append("✗ 配置文件: 未找到server.xml")
            return {
                "rule_name": "日志审计检查",
                "status": "未通过",
                "config_status": {"config_exists": False},
                "status_details": status_details,
                "recommendation": "请先确认Tomcat配置文件位置"
            }

        # 检查日志配置
        content = TomcatSecurityRules.run_cmd_simple(f"cat {config_path}")
        access_log_valve = re.search(r'<Valve\s+className="org.apache.catalina.valves.AccessLogValve"', content)
        log_dir = f"{TomcatSecurityRules.TOMCAT_INSTALL_PATH}/logs"

        # 检查访问日志配置
        if access_log_valve:
            status_details.append("✓ 访问日志: 已配置AccessLogValve")
            # 检查日志目录
            if TomcatSecurityRules.run_cmd_simple(f"test -d {log_dir} && echo 1 || echo 0") == "1":
                log_files = TomcatSecurityRules.run_cmd_simple(f"ls {log_dir}/access_log.* 2>/dev/null")
                if log_files:
                    status_details.append(f"✓ 日志文件: 存在访问日志文件")
                else:
                    issues.append("未生成访问日志文件")
                    status_details.append(f"✗ 日志文件: 未发现访问日志文件")
            else:
                issues.append(f"日志目录{log_dir}不存在")
                status_details.append(f"✗ 日志目录: {log_dir} 不存在")
        else:
            issues.append("未配置访问日志")
            status_details.append("✗ 访问日志: 未配置AccessLogValve")

        # 检查日志轮转
        logrotate_check = TomcatSecurityRules.run_cmd_simple(
            f"grep '{TomcatSecurityRules.TOMCAT_INSTALL_PATH}/logs' /etc/logrotate.d/ 2>/dev/null | grep -v '#'")
        if logrotate_check:
            status_details.append("✓ 日志轮转: 已配置")
        else:
            issues.append("未配置日志轮转策略")
            status_details.append("✗ 日志轮转: 未配置（可能导致日志文件过大）")

        return {
            "rule_name": "日志审计检查",
            "status": "通过" if not issues else "未通过",
            "config_status": {
                "access_log_enabled": bool(access_log_valve),
                "log_dir_exists": TomcatSecurityRules.run_cmd_simple(f"test -d {log_dir} && echo 1 || echo 0") == "1",
                "logrotate_enabled": bool(logrotate_check)
            },
            "status_details": status_details,
            "recommendation": "日志审计配置完整" if not issues else f"需修复: {'; '.join(issues)}"
        }

    @staticmethod
    def check_data_backup() -> Dict[str, Any]:
        """4. 检查重要数据备份策略"""
        print("[Tomcat规则4] 检查数据备份策略...")
        status_details = []
        issues = []

        # Tomcat重要目录
        important_dirs = [
            f"{TomcatSecurityRules.TOMCAT_INSTALL_PATH}/conf",  # 配置文件目录
            f"{TomcatSecurityRules.TOMCAT_INSTALL_PATH}/webapps",  # 应用目录
            f"{TomcatSecurityRules.TOMCAT_INSTALL_PATH}/lib"  # 库文件目录
        ]

        # 检查备份文件或目录
        backup_signs = []
        for dir_path in important_dirs:
            if TomcatSecurityRules.run_cmd_simple(f"test -d {dir_path} && echo 1 || echo 0") == "1":
                # 检查最近7天的备份
                backups = TomcatSecurityRules.run_cmd_simple(
                    f"find {dir_path} ../$(basename {dir_path})_backup* -maxdepth 0 -type d -mtime -7 2>/dev/null"
                )
                if backups:
                    backup_signs.extend(backups.splitlines())

        # 检查备份脚本
        backup_scripts = TomcatSecurityRules.run_cmd_simple(
            f"grep -r '{TomcatSecurityRules.TOMCAT_INSTALL_PATH}' /etc/cron* /var/spool/cron 2>/dev/null | grep -E 'tar|cp|rsync'"
        )

        if backup_signs or backup_scripts:
            status_details.append("✓ 备份状态: 存在近期备份或备份脚本")
            if backup_signs:
                status_details.append(f"  最近备份: {backup_signs[0]}")
            if backup_scripts:
                status_details.append(f"  备份脚本: 存在定时任务配置")
        else:
            issues.append("未发现近期备份或自动备份策略")
            status_details.append("✗ 备份状态: 无有效备份机制")

        return {
            "rule_name": "重要数据备份检查",
            "status": "通过" if not issues else "未通过",
            "config_status": {
                "has_recent_backup": bool(backup_signs),
                "has_backup_script": bool(backup_scripts)
            },
            "status_details": status_details,
            "recommendation": "备份策略配置完善" if not issues else "建议配置定时备份配置文件、应用目录和库文件目录"
        }

    @staticmethod
    def check_maintenance_patches() -> Dict[str, Any]:
        """5. 检查定期维护和补丁策略"""
        print("[Tomcat规则5] 检查维护和补丁策略...")
        status_details = []
        issues = []

        # 检查当前Tomcat版本
        version_output = TomcatSecurityRules.run_cmd_simple(
            f"{TomcatSecurityRules.TOMCAT_INSTALL_PATH}/bin/version.sh 2>&1"
        )
        version_match = re.search(r'Tomcat/(\d+\.\d+\.\d+)', version_output)
        current_version = version_match.group(1) if version_match else "未知"
        status_details.append(f"✓ 当前版本: {current_version}")

        # 检查是否有可用更新（简单检查官方最新版本）
        # 实际环境中可能需要更复杂的版本比较逻辑
        update_check = TomcatSecurityRules.run_cmd_simple(
            "curl -s https://tomcat.apache.org/download-80.cgi | grep -o '8\.[0-9]\+\.[0-9]\+' | head -n 1"
        )
        if update_check and version_match and update_check > version_match.group(1):
            issues.append(f"存在可用的Tomcat更新（最新版本: {update_check}）")
            status_details.append(f"✗ 版本更新: 当前版本{current_version}，发现新版本{update_check}")
        else:
            status_details.append("✓ 版本更新: 当前为最新版本或无法验证")

        # 检查自动更新配置
        auto_update_check = TomcatSecurityRules.run_cmd_simple("""
            # 检查yum-cron或dnf-automatic服务是否启用
            systemctl is-enabled yum-cron >/dev/null 2>&1 || 
            systemctl is-enabled dnf-automatic.timer >/dev/null 2>&1 || 
            # 检查包含系统更新的定时任务（不局限于Tomcat路径）
            grep -r -i 'yum update\|dnf update\|yum upgrade\|dnf upgrade' /etc/cron* /var/spool/cron 2>/dev/null
        """)
        auto_update = auto_update_check
        if auto_update:
            status_details.append("✓ 自动更新: 已配置定期更新脚本")
        else:
            issues.append("未配置自动更新策略")
            status_details.append("✗ 自动更新: 未配置定期更新")

        return {
            "rule_name": "维护和补丁策略检查",
            "status": "通过" if not issues else "未通过",
            "config_status": {
                "current_version": current_version,
                "latest_version": update_check if update_check else "未知",
                "auto_update_enabled": bool(auto_update)
            },
            "status_details": status_details,
            "recommendation": "维护策略完善" if not issues else f"需修复: {'; '.join(issues)}"
        }

    @staticmethod
    def check_directory_listing() -> Dict[str, Any]:
        """6. 检查是否禁止目录列表"""
        print("[Tomcat规则6] 检查目录列表禁止配置...")
        status_details = []
        issues = []
        web_xml = f"{TomcatSecurityRules.TOMCAT_INSTALL_PATH}/conf/web.xml"

        # 检查web.xml是否存在
        if TomcatSecurityRules.run_cmd_simple(f"test -f {web_xml} && echo 1 || echo 0") != "1":
            issues.append("未找到web.xml配置文件")
            status_details.append("✗ 配置文件: 未找到web.xml")
            return {
                "rule_name": "禁止目录列表检查",
                "status": "未通过",
                "config_status": {"config_exists": False},
                "status_details": status_details,
                "recommendation": "请先确认web.xml配置文件位置"
            }

        # 检查默认servlet的listings配置
        content = TomcatSecurityRules.run_cmd_simple(f"cat {web_xml}")
        default_servlet = re.search(
            r'<servlet>\s*<servlet-name>default</servlet-name>.*?<init-param>.*?</servlet>',
            content,
            re.DOTALL | re.IGNORECASE
        )

        if default_servlet:
            listings_match = re.search(
                r'<init-param>\s*<param-name>listings</param-name>\s*<param-value>(\w+)</param-value>',
                default_servlet.group(0),
                re.IGNORECASE
            )
            if listings_match:
                listings_value = listings_match.group(1).lower()
                if listings_value == "false":
                    status_details.append("✓ 目录列表: 已禁用（全局配置）")
                else:
                    issues.append("全局默认配置允许目录列表")
                    status_details.append("✗ 目录列表: 全局配置允许目录列表")
            else:
                issues.append("未配置listings参数，默认允许目录列表")
                status_details.append("✗ 目录列表: 未配置listings参数（默认允许）")
        else:
            issues.append("未找到默认servlet配置")
            status_details.append("✗ 配置检查: 未找到default servlet配置")

        # 检查应用特定配置
        webapps_dir = f"{TomcatSecurityRules.TOMCAT_INSTALL_PATH}/webapps"
        if TomcatSecurityRules.run_cmd_simple(f"test -d {webapps_dir} && echo 1 || echo 0") == "1":
            app_web_xmls = TomcatSecurityRules.run_cmd_simple(
                f"find {webapps_dir} -name 'web.xml' -path '*/WEB-INF/web.xml'"
            ).splitlines()
            for app_xml in app_web_xmls:
                app_content = TomcatSecurityRules.run_cmd_simple(f"cat {app_xml}")
                if re.search(r'<param-name>listings</param-name>.*<param-value>true</param-value>',
                             app_content, re.DOTALL | re.IGNORECASE):
                    app_name = re.search(f'{webapps_dir}/([^/]+)/', app_xml).group(1)
                    issues.append(f"应用{app_name}单独开启了目录列表")
                    status_details.append(f"✗ 应用配置: {app_name} 允许目录列表")

        return {
            "rule_name": "禁止目录列表检查",
            "status": "通过" if not issues else "未通过",
            "config_status": {
                "global_listings_disabled": "✓ 目录列表: 已禁用（全局配置）" in status_details,
                "app_listings_count": len([d for d in status_details if "✗ 应用配置" in d])
            },
            "status_details": status_details,
            "recommendation": "目录列表已正确禁用" if not issues else
            "需在web.xml中设置default servlet的listings为false，并检查所有应用的配置"
        }


    @staticmethod
    def get_all_rules() -> List[callable]:
        """获取所有Tomcat审计规则的函数列表"""
        return [
            TomcatSecurityRules.check_strong_password,
            TomcatSecurityRules.check_minimal_permissions,
            TomcatSecurityRules.check_log_audit,
            TomcatSecurityRules.check_data_backup,
            TomcatSecurityRules.check_maintenance_patches,
            TomcatSecurityRules.check_directory_listing
        ]